/*
tmsg.hpp

Licensed under the MIT License <http://opensource.org/licenses/MIT>.
SPDX-License-Identifier: MIT
Copyright (c) 2018 zjzengdongyang <http://zjzdy.cn>.

Permission is hereby  granted, free of charge, to any  person obtaining a copy
of this software and associated  documentation files (the "Software"), to deal
in the Software  without restriction, including without  limitation the rights
to  use, copy,  modify, merge,  publish, distribute,  sublicense, and/or  sell
copies  of  the Software,  and  to  permit persons  to  whom  the Software  is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE  IS PROVIDED "AS  IS", WITHOUT WARRANTY  OF ANY KIND,  EXPRESS OR
IMPLIED,  INCLUDING BUT  NOT  LIMITED TO  THE  WARRANTIES OF  MERCHANTABILITY,
FITNESS FOR  A PARTICULAR PURPOSE AND  NONINFRINGEMENT. IN NO EVENT  SHALL THE
AUTHORS  OR COPYRIGHT  HOLDERS  BE  LIABLE FOR  ANY  CLAIM,  DAMAGES OR  OTHER
LIABILITY, WHETHER IN AN ACTION OF  CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE  OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
*/

#ifndef TMSG_HPP
#define TMSG_HPP

//编译器版本检测
#if defined(_MSC_VER)
# if _MSC_VER < 1900
#  error "tmsg only support C++11 or late"
# endif
#elif defined(__cplusplus)
# if __cplusplus < 201103L
#  error "tmsg only support C++11 or late"
# endif
#endif

#if defined(_MSC_VER) && (_MSC_VER >= 1200)
# pragma once
#endif // defined(_MSC_VER) && (_MSC_VER >= 1200)


#include <string>
#include <vector>
#include <map>
#include <algorithm>
#include <chrono>
#include <memory>
#include <mqtt/client.h>
#include <msgpack.hpp>

//更多mqtt说明可以参考https://mcxiaoke.gitbooks.io/mqtt-cn/content/
//数据传输格式采用msgpack,更多说明参考https://msgpack.org/
//如果需要在 JSON, CBOR, MessagePack(msgpack), UBJSON 这4者之间简单转化,请使用https://github.com/nlohmann/json
class tmsg {
public:
    //消息类
    class msg {
    public:
        msg() : sbuf(1024), parsed(false) { updateTime(); }
        msg(msgpack::sbuffer &buf, std::vector<std::string> &name) { msg(buf.data(), buf.size(), name); }
        msg(const char* buf, size_t len, std::vector<std::string> &name) : sbuf(len+1), names(name), parsed(false) {
            sbuf.write(buf, len);
            updateTime();
        }

        /**
         * 打包数据
         * @tparam T 数据类型
         * @param name 数据的名称
         * @param v 待打包的数据
         */
        template<typename T>
        inline void pack(const std::string &name, const T &v);
        /**
         * 取得数据,将内部数据转换成v的数据类型并储存到v
         * 其中,按数据的名称来检索对应的数据,将会按第一个与名称匹配的数据进行转换
         * 如果名称不存在,则直接返回
         * @tparam T 数据类型
         * @param name 数据的名称
         * @param v 数据
         * @return 内部数据转换是否转换成功
         */
        template<typename T>
        inline bool get(const std::string &name, T &v);
        /**
         * 取得数据,将内部数据转换成需要的数据类型并返回它
         * 其中,按数据的名称来检索对应的数据,将会按第一个与名称匹配的数据进行转换
         * 如果数据转换失败或者名称不存在,将会返回该数据类型的无参数构造函数构造的对象
         * @tparam T 数据类型
         * @param name 数据的名称
         * @return 转换好的数据
         */
        template<typename T>
        inline T get(const std::string &name);
        /**
         * 取得数据,将序号为sequence的内部数据转换成v的数据类型并储存到v
         * 如果序号大于等于数据数量,则直接返回
         * @tparam T 数据类型
         * @param sequence 待转换的数据的序号(从0开始计算)
         * @param v 数据
         * @return 内部数据转换是否转换成功
         */
        template<typename T>
        inline bool get(unsigned long sequence, T &v);
        /**
         * 取得数据,将序号为sequence的内部数据转换成需要的数据类型并返回它
         * 如果数据转换失败或者序号大于等于数据数量,将会返回该数据类型的无参数构造函数构造的对象
         * @tparam T 数据类型
         * @param sequence 待转换的数据的序号(从0开始计算)
         * @return 转换好的数据
         */
        template<typename T>
        inline T get(unsigned long sequence);
        /**
         * 获取序号为sequence的数据名称(标识)
         * @param sequence 数据的序号(从0开始计算)
         * @return 数据名称(标识)
         */
        std::string getName(unsigned long sequence);
        /**
         * 清空数据并重置时间
         */
        void clear();
        /**
         * 将数据转成json格式
         * @param is_names 如果为true,将转换名称列表,否则转换数据
         * @return json数据
         */
        std::string toJson(bool is_names = false);
        /**
         * 获取数据的数量
         * @return 数据数量
         */
        size_t dataCount();
        /**
         * 获取本消息被生成时的时间, 可以用这个来判断tmsg获取到这个消息时间与现在相差多久
         * @return 生成时间
         */
        std::chrono::steady_clock::time_point getTime();

    private:
        void parse();//解包数据
        void updateTime();//更新时间
        friend class tmsg;
        msgpack::sbuffer sbuf;//打包的数据的buffer
        std::vector<std::string> names;//数据名称列表
        std::vector<msgpack::object_handle> parsedData;//解包后的数据列表
        bool parsed;//是否已解包
        std::chrono::steady_clock::time_point time;//生成时间
    };
    //用来给用户定义的新消息回调函数的类
    class newMsgCallback {
    public:
        virtual ~newMsgCallback() {}
        /**
         * 新消息到达时会调用这个函数
         * @param topic 这个新消息所属的主题
         * @param newMsg 新消息的指针
         */
        virtual void message_arrived(const std::string &topic, std::shared_ptr<msg> newMsg) {}
    };

    /**
     * 构造tmsg
     * @param serverURI 服务器URI
     * @param clientId 本客户端名称
     * @param maxBufferedMsg 消息缓存上限(条为单位)
     */
    explicit tmsg(const std::string &serverURI, const std::string &clientId = std::string(), int maxBufferedMsg = 10);
    ~tmsg();
    /**
     * 连接到mqtt服务器(使用内置的配置)
     * @return 是否连接成功
     */
    bool connect();
    /**
     * 连接到mqtt服务器
     * @param connOpts 连接参数
     * @return 是否连接成功
     */
    bool connect(mqtt::connect_options &connOpts);
    /**
     * 发布数据
     * 主题说明:
     * 斜杠('/' U+002F)用于分割主题的每个层级,在tmsg的函数publish与subscribe中不需要以'/'开头
     * 主题名和主题过滤器是区分大小写的。
     * 主题名和主题过滤器可以包含空格。
     * 主题名和主题过滤器不能包含空字符 (Unicode U+0000)
     * 主题名和主题过滤器是UTF-8编码字符串，它们不能超过65510(=65535-25, 25用于tmsg标识)字节
     * qos参数说明:
     * QoS等级越高越消耗资源
     * QoS 0: 最多分发一次
     * 消息的分发依赖于底层网络的能力。接收者不会发送响应，发送者也不会重试。消息可能送达一次也可能根本没送达。
     * QoS 1: 至少分发一次
     * 服务质量确保消息至少送达一次。
     * QoS 2: 仅分发一次
     * 这是最高等级的服务质量，消息丢失和重复都是不可接受的。使用这个服务质量等级会有额外的开销。
     * retained参数说明:
     * 如果为retained为true, 那么这条信息为保留信息
     * 保留信息与普通信息的不同之处在于,一个客户端新订阅这个主题,客户端会立刻收到之前这个主题下发布的最后一条保留信息,而其他的历史信息则不会收到
     * 建议重要指令以保留信息发布, 一旦有客户端挂掉再次重新连接仍能收到当前需要执行的指令
     * 建议配置数据与部分元数据按保留信息发布, 以便新客户端能收到
     * @param dataTopic 数据要发布到的主题,不可为空
     * @param m 数据/消息
     * @param qos 发送给服务器的数据的服务质量等级
     * @param retained 是否按保留信息发布
     */
    void publish(const std::string &dataTopic, const msg &m, int qos = 2, bool retained = false);
    void publish(const std::string &dataTopic, const std::shared_ptr<msg> &m, int qos = 2, bool retained = false);
    /**
     * 订阅主题
     * 其中订阅的主题可以使用通配符:
     * 多层通配符('#' U+0023)是用于匹配主题中任意层级的通配符,表示它的父级和任意数量的子层级
     * 多层通配符必须位于'/'后面或只有它一个.不管哪种情况,它都必须是主题过滤器的最后一个字符
     * 单层通配符('+' U+002B)是只能用于单个主题层级匹配的通配符
     * 在主题过滤器的任意层级都可以使用单层通配符,包括第一个和最后一个层级,然而它必须占据过滤器的整个层级
     * 可以在主题过滤器中的多个层级中使用单层通配符.也可以和多层通配符一起使用
     * 其他详见@see publish
     * @param dataTopic 要订阅的主题,不可为空
     * @param qos 从服务器接收数据的服务质量等级,qos说明详见@see publish
     */
    void subscribe(const std::string &dataTopic, int qos = 2);
    /**
     * 获取当前服务器连接状态
     * @return 服务器是否已连接
     */
    bool is_connected();
    /**
     * 断开服务器连接
     * @return 断开是否成功
     */
    bool disconnect();
    /**
     * 获取最近一条在主题dataTopic下收到的信息
     * @param dataTopic 信息所属的主题
     * @return 信息的智能指针,当该主题没有收到过信息时为空指针
     */
    std::shared_ptr<msg> getLatestMsg(const std::string &dataTopic);
    /**
     * 设置信息输出等级
     * @param level 信息输出等级
     */
    void setVerbose(unsigned int level = 1);
    /**
     * 设置当新消息到达时调用的回调函数
     * @param cb 继承newMsgCallback,并实现了message_arrived这个回调函数的对象, 需要用户自行保证这个对象的生命周期
     */
    void setNewMsgCallback(newMsgCallback &cb);
    /**
     * 取得所有已收到消息的被订阅的主题
     * @return 主题列表
     */
    std::vector<std::string> getAllTopic();
    /**
     * 取得所有的已知数据定义的引用,map内容为<主题, <定义vid, 定义>>
     * @return 所有数据的定义map
     */
    const std::map<std::string, std::pair<unsigned char, std::vector<std::string>>>& getAllDefines();

private:
    mqtt::client cli;
    std::map<std::string, std::pair<unsigned char, std::vector<std::string>>> defines;//数据名称定义<主题, <定义vid, 定义>>
    std::map<std::string, std::shared_ptr<msg>> msgs;//信息池<主题, 信息指针>
    unsigned int verbose;//输出等级
    newMsgCallback* userCallback;//用户回调

    //mqtt的回调函数, 用于记录掉线与接收数据
    class callback : public virtual mqtt::callback {
        tmsg& tm_;
        void connection_lost(const std::string& cause) override;//连接丢失
        void message_arrived(mqtt::const_message_ptr msg) override;//收到数据
    public:
        explicit callback(tmsg& tm);
    } cb;
    friend class callback;
};

template<typename T>
inline void tmsg::msg::pack(const std::string &name, const T &v) {
    parsed = false;
    names.emplace_back(name);
    msgpack::pack(sbuf, v);
}

template<typename T>
inline bool tmsg::msg::get(const std::string &name, T &v) {
    auto result = std::find(names.cbegin(), names.cend(), name);//找第一个与name匹配的数据名称的序号
    if(result != names.cend())
        return get(std::distance(names.cbegin(), result), v);
    return false;
}

template<typename T>
inline T tmsg::msg::get(const std::string &name) {
    T v;
    get(name, v);
    return v;
}

template<typename T>
inline bool tmsg::msg::get(unsigned long sequence, T &v) {
    if(!parsed)//确保数据已解析
        parse();
    if(sequence < parsedData.size()) {//判断序号是否正常
        try {
            parsedData[sequence].get().convert(v);//转换数据
            return true;
        }
        catch(msgpack::type_error const& e) {
            std::cerr << e.what() << std::endl;
        }
    }
    return false;
}

template<typename T>
inline T tmsg::msg::get(unsigned long sequence) {
    T v;
    get(sequence, v);
    return v;
}

#ifdef TMSG_HEADERONLY
#include "tmsg.cpp"
#endif

#endif //TMSG_HPP
